package com.dbflow5.transaction;


import com.dbflow5.config.DBFlowDatabase;
import com.dbflow5.config.FlowLog;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;

import java.util.function.BiFunction;
import java.util.function.Function;

import com.dbflow5.annotation.WorkerThread;

/**
 * Description: The main transaction class. It represents a transaction that occurs in the database.
 * This is a handy class that allows you to wrap up a set of database modification (or queries) into
 * a code block that gets accessed all on the same thread, in the same queue. This can prevent locking
 * and synchronization issues when trying to read and write from the database at the same time.
 * <p>
 * <p>
 * To create one, the recommended method is to use the [DBFlowDatabase.beginTransactionAsync].
 */
public class Transaction<R> {

    public static final EventHandler transactionHandler = new EventHandler(EventRunner.getMainEventRunner());

    ITransaction<R> transaction;
    private DBFlowDatabase databaseDefinition;

    Function<Transaction<R>, Void> ready = null;
    BiFunction<Transaction<R>, Throwable, Void> error;
    BiFunction<Transaction<R>, R, Void> success;
    Function<Transaction<R>, Void> completion;

    String name;
    private boolean shouldRunInTransaction = true;
    private boolean runCallbacksOnSameThread = true;

    public Transaction(ITransaction<R> transaction, DBFlowDatabase databaseDefinition, Function<Transaction<R>, Void> ready,
                       BiFunction<Transaction<R>, Throwable, Void> error, BiFunction<Transaction<R>, R, Void> success, Function<Transaction<R>, Void> completion,
                       String name, boolean shouldRunInTransaction, boolean runCallbacksOnSameThread){
        this.transaction = transaction;
        this.databaseDefinition = databaseDefinition;
        this.ready = ready;
        this.error = error;
        this.success = success;
        this.completion = completion;
        this.name = name;
        this.shouldRunInTransaction = shouldRunInTransaction;
        this.runCallbacksOnSameThread = runCallbacksOnSameThread;
    }

    public Transaction(Builder<R> builder){
        databaseDefinition = builder.database;
        error = builder.errorCallback;
        success = builder.successCallback;
        completion = builder.completion;
        transaction = builder.transaction;
        name = builder.name;
        shouldRunInTransaction = builder.shouldRunInTransaction;
        runCallbacksOnSameThread = builder.runCallbacksOnSameThread;
    }

    /**
     * Runs the transaction in the [BaseTransactionManager] of the associated database.
     * @return Transaction
     */
    public Transaction<R> execute() {
        databaseDefinition.transactionManager.addTransaction(this);
        return this;
    }

    /**
     * Cancels a transaction that has not run yet.
     */
    public void cancel() {
        databaseDefinition.transactionManager.cancelTransaction(this);
    }

    /**
     * Executes the transaction immediately on the same thread from which it is called. This calls
     * the [DBFlowDatabase.executeTransaction] method, which runs the
     * [.transaction] in a database transaction.
     */
    @WorkerThread
    public void executeSync() {
        try {
            if(ready != null) {
                ready.apply(this);
            }

            R result;
            if (shouldRunInTransaction) {
                result = databaseDefinition.executeTransaction(transaction);
            } else {
                result = transaction.execute(databaseDefinition);
            }
            if (success != null) {
                if (runCallbacksOnSameThread) {
                    success.apply(this, result);
                    complete();
                } else {
                    transactionHandler.postTask(() -> {
                        success.apply(Transaction.this, result);
                        complete();
                    });
                }
            }
        } catch (Throwable throwable){
            FlowLog.logError(throwable);
            if (error != null) {
                if (runCallbacksOnSameThread) {
                    error.apply(this, throwable);
                    complete();
                } else {
                    transactionHandler.postTask(() -> {
                        error.apply(this, throwable);
                        complete();
                    });
                }
            } else {
                throw new RuntimeException("An exception occurred while executing a transaction:"+throwable.getMessage(), throwable);
            }
        }
    }

    private void complete() {
        if(completion != null) {
            completion.apply(this);
        }
    }

    public Builder<R> newBuilder() {
        return new Builder<R>(transaction, databaseDefinition)
                .error(error)
                .success(success)
                .name(name)
                .shouldRunInTransaction(shouldRunInTransaction)
                .runCallbacksOnSameThread(runCallbacksOnSameThread);
    }

    /**
     * The main entry point into [Transaction], this provides an easy way to build up transactions.
     */
    public static class Builder<R> {

        /**
         * @param transaction The interface that actually executes the transaction.
         * @param database    The database this transaction will run on. Should be the same
         *                    DB as the code that the transaction runs in.
         */
        public Builder(ITransaction<R> transaction, DBFlowDatabase database) {
            this.transaction = transaction;
            this.database = database;
        }

        ITransaction<R> transaction;
        DBFlowDatabase database;

        Function<Transaction<R>, Void> ready = null;
        BiFunction<Transaction<R>, Throwable, Void> errorCallback = null;
        BiFunction<Transaction<R>, R, Void> successCallback = null;
        Function<Transaction<R>, Void> completion = null;
        String name = null;
        boolean shouldRunInTransaction = true;
        boolean runCallbacksOnSameThread = false;

        /**
         * Specify a callback when the transaction is ready to execute. Do an initialization here,
         * and cleanup on [completion]
         *
         * @param ready ready
         * @return builder
         */
        Builder<R> ready(Function<Transaction<R>, Void> ready) {
            this.ready = ready;
            return this;
        }

        /**
         * Specify an error callback to return all and any [Throwable] that occurred during a [Transaction].
         * @param error Invoked on the UI thread, unless [runCallbacksOnSameThread] is true.
         * @return builder
         */
        public Builder<R> error(BiFunction<Transaction<R>, Throwable, Void> error) {
            this.errorCallback = error;
            return this;
        }

        /**
         * Specify a listener for successful transactions. This is called when the [ITransaction]
         * specified is finished and it is posted on the UI thread.
         *
         * @param success The callback, invoked on the UI thread, unless [runCallbacksOnSameThread] is true.
         * @return builder
         */
        public Builder<R> success(BiFunction<Transaction<R>, R, Void> success) {
            this.successCallback = success;
            return this;
        }

        /**
         * Runs exactly once, no matter if it was successful or failed, at the end of the execution
         * of this transaction.
         *
         * @param completion Invoked on the UI thread, unless [runCallbacksOnSameThread] is true.
         * @return builder
         */
        public Builder<R> completion(Function<Transaction<R>, Void> completion) {
            this.completion = completion;
            return this;
        }

        /**
         * Give this transaction a name. This will allow you to call [ITransactionQueue.cancel].
         *
         * @param name The name of this transaction. Should be unique for any transaction currently
         * running in the [ITransactionQueue].
         * @return builder
         */
        Builder<R> name(String name) {
            this.name = name;
            return this;
        }

        /**
         * A new instance of [Transaction]
         *
         * @param shouldRunInTransaction True is default. If true, we run this [Transaction] in
         * a database transaction. If this is not necessary (usually for
         * [QueryTransaction]), you should specify false.
         * @return A new instance
         */
        public Builder<R> shouldRunInTransaction(boolean shouldRunInTransaction) {
            this.shouldRunInTransaction = shouldRunInTransaction;
            return this;
        }

        /**
         * If true we return the callbacks from his [Transaction] on the same thread we call
         *
         * @param runCallbacksOnSameThread Default is false. If true we return the callbacks from
         * this [Transaction] on the same thread we call
         * [.execute] from.
         * @return If true we return the callbacks from his [Transaction]
         */
        public Builder<R> runCallbacksOnSameThread(boolean runCallbacksOnSameThread) {
            this.runCallbacksOnSameThread = runCallbacksOnSameThread;
            return this;
        }

        /**
         * A new instance of [Transaction]
         *
         * @return A new instance of [Transaction]. Subsequent calls to this method produce
         * new instances.
         */
        public Transaction<R> build(){
            return new Transaction<>(this);
        }

        /**
         * Convenience method to simply execute a transaction.
         *
         * @param ready ready
         * @param error error
         * @param completion completion
         * @param success success
         * @return transaction
         */
        public Transaction<R> execute(
                Function<Transaction<R>, Void> ready,
                BiFunction<Transaction<R>, Throwable, Void> error,
                Function<Transaction<R>, Void> completion,
                BiFunction<Transaction<R>, R, Void> success){

            this.ready(ready);
            this.success(success);
            this.error(error);
            this.completion(completion);

            return build().execute();
        }
    }

}

